This chapter
is taken from book "Programming Windows Phone 7" by Charles Petzold published by
Microsoft press.
http://www.charlespetzold.com/phone/index.html
Databases and Business Objects:
In general, you'll be filling an ItemsControl or ListBox with those vague but
ubiquitous entities known as business objects. I'm going to spend the remainder
of the chapter focusing on programs that use a database of high school students.
In these examples, the database is downloaded from a directory on my web site,
but because I want to focus solely on the presentation of this data in this
chapter, changes to properties of the Student
class will be simulated locally.
Heare is a library project named ElPasoHighSchool that
contains several classes to read the XML file from my Web site and deserialize
it into .NET objects and also a Student
class, it implements
INotifyPropertyChanged
and has several properties pertaining to the student,
including name, sex, a filename referencing the photograph, and a grade point
average:
namespace
ElPasoHighSchool
{
public
class Student :
INotifyPropertyChanged
{
public event
PropertyChangedEventHandler PropertyChanged;
string fullName;
string firstName;
string middleName;
string lastName;
string sex;
string photoFilename;
decimal gradePointAverage;
public string
FullName
{
set
{
if (fullName != value)
{
fullName =
value;
OnPropertyChanged("FullName");
}
}
get
{
return fullName;
}
}
.....
public string
Sex
{
set
{
if (sex !=
value)
{
sex =
value;
OnPropertyChanged("Sex");
}
}
get
{
return sex;
}
}
public string
PhotoFilename
{
set
{
if (photoFilename !=
value)
{
photoFilename =
value;
OnPropertyChanged("PhotoFilename");
}
}
get
{
return photoFilename;
}
}
public decimal
GradePointAverage
{
set
{
if (gradePointAverage !=
value)
{
gradePointAverage =
value;
OnPropertyChanged("GradePointAverage");
}
}
get
{
return gradePointAverage;
}
}
protected virtual
void OnPropertyChanged(string
propChanged)
{
if (PropertyChanged !=
null)
PropertyChanged(this,
new PropertyChangedEventArgs(propChanged));
}
}
}
There will be one instance of the Student class for each student. Changes to
any of these properties cause a PropertyChanged
event to fire. Thus, this class is suitable as a source for
data bindings.
The
StudentBody class also implements INotifyPropertyChanged:
namespace
ElPasoHighSchool
{
public
class StudentBody :
INotifyPropertyChanged
{
public event
PropertyChangedEventHandler PropertyChanged;
string school;
ObservableCollection<Student>
students = new ObservableCollection<Student>();
.....
protected virtual
void OnPropertyChanged(string
propChanged)
{
if (PropertyChanged !=
null)
PropertyChanged(this,
new PropertyChangedEventArgs(propChanged));
}
}
}
This class contains a property indicating the name of the school and an
ObservableCollection of type Student to store all the Student objects.
ObservableCollection is a very popular collection class in Silverlight because
it implements the INotifyCollectionChanged interface, which means that it fires
a CollectionChanged event whenever an
item is added to or removed from the collection.
Before continuing, let's take a look at an excerpt of
the student.xml file, which available in source code please download from there:
As you can see, the element tags correspond to
properties in the Student and
StudentBody classes. I created this file using XML serialization with the
XmlSerializer class, and XML deserialization can convert it back into Student
and StudentBody objects. That is the function of the StudentBodyPresenter class,
which again implements INotifyPropertyChanged:
namespace
ElPasoHighSchool
{
public
class StudentBodyPresenter :
INotifyPropertyChanged
{
public event
PropertyChangedEventHandler PropertyChanged;
StudentBody studentBody;
Random rand =
new Random();
public StudentBodyPresenter()
{
Uri uri = new
Uri("http://www.charlespetzold.com/Students/students.xml");
// , UriKind.Relative);
WebClient webClient =
new WebClient();
webClient.DownloadStringCompleted += OnDownloadStringCompleted;
webClient.DownloadStringAsync(uri);
}
....
protected virtual
void OnPropertyChanged(string
propChanged)
{
if (PropertyChanged !=
null)
PropertyChanged(this,
new PropertyChangedEventArgs(propChanged));
}
....
}
}
You can begin experimenting with this database by opening up a new
Silverlight project, making a reference to the ElPasoHighSchool.dll library, and
putting an XML namespace declaration in the MainPage.xaml file:
xmlns:elpaso="clr-namespace:ElPasoHighSchool;assesmbly=ElPasoHighSchool"
You then instantiate this the StudentBodyPresenter class in the Resources
collection:
<phone:PhoneApplicationPage.Resources>
<elpaso:StudentBodyPresenter
x:Key="studentBodyPresenter" />
</phone:PhoneApplicationPage.Resources>
You can then put a TextBlock in the content area with a binding to that
resource:
<Grid x:Name="ContentPanel"
Grid.Row="1" Margin="12,0,12,0">
<TextBlock
HorizontalAlignment="Center"
VerticalAlignment="Center"
Text="{Binding
Source={StaticResource
studentBodyPresenter},
Path=StudentBody.School}" />
</Grid>
The screen indicates that the program is successfully downloading and
deserializing the students.xml file:
The SelectedItem property of the ListBox is of type Student, so the binding
path can reference a property of Student, such as FullName. Now when an item is
selected from the ListBox, the TextBlock displays the item's FullName property:
<Grid
x:Name="ContentPanel"
Grid.Row="1"
Margin="12,0,12,0"
DataContext="{Binding
Source={StaticResource
studentBodyPresenter},
Path=StudentBody}">
<Grid.RowDefinitions>
<RowDefinition
Height="*"
/>
<RowDefinition
Height="Auto"
/>
</Grid.RowDefinitions>
<ListBox
Grid.Row="0"
Name="listBox"
ItemsSource="{Binding
Students}"
DisplayMemberPath="FullName"
/>
<TextBlock
Grid.Row="1"
FontSize="{StaticResource
PhoneFontSizeLarge}"
HorizontalAlignment="Center"
Text="{Binding
ElementName=listBox,
Path=SelectedItem.FullName}"
/>
</Grid>
Or, replace the TextBlock with an Image element:
<Grid
x:Name="ContentPanel"
Grid.Row="1"
Margin="12,0,12,0"
DataContext="{Binding
Source={StaticResource
studentBodyPresenter},
Path=StudentBody}">
<Grid.RowDefinitions>
<RowDefinition
Height="*"
/>
<RowDefinition
Height="Auto"
/>
</Grid.RowDefinitions>
<ListBox
Grid.Row="0"
Name="listBox"
ItemsSource="{Binding
Students}"
DisplayMemberPath="FullName"
/>
<Image
Grid.Row="1"
HorizontalAlignment="Center"
Stretch="None"
Source="{Binding
ElementName=listBox,
Path=SelectedItem.PhotoFilename}"
/>
</Grid>
You can now go through the ListBox and select an item to view that student's
picture:
Fun with DataTemplates
For the remainder of this article, I want to switch from the ListBox
to the ItemsControl to focus solely on presentation and navigation rather than
selection. To play along, you can create a new project, set a reference to the
ElPasoHighSchool library, and in the XAML file add an XML namespace declaration
for that library and instantiate the StudentBodyPresenter class in the Resources collection as in the previous program,
just Replace the
DisplayMemberPath with a DataTemplateto
provide more extensive information, nicely formatted:
<Grid
x:Name="ContentPanel"
Grid.Row="1"
Margin="12,0,12,0">
<ScrollViewer>
<ItemsControl
ItemsSource="{Binding
Students}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border
BorderBrush="{StaticResource
PhoneAccentBrush}"
BorderThickness="1"
CornerRadius="12"
Margin="2">
<Grid>
<Grid.RowDefinitions>
<RowDefinition
Height="*"
/>
<RowDefinition
Height="*"
/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition
Width="Auto"
/>
<ColumnDefinition
Width="*"
/>
</Grid.ColumnDefinitions>
<Image
Grid.Row="0"
Grid.Column="0"
Grid.RowSpan="2"
Source="{Binding
PhotoFilename}"
Height="120"
Width="90"Margin="6"
/>
<StackPanel
Grid.Row="0"
Grid.Column="1"
Orientation="Horizontal"
VerticalAlignment="Center">
<TextBlock
Text="{Binding
LastName}"
/>
<TextBlock
Text=",
" />
<TextBlock
Text="{Binding
FirstName}"
/>
<TextBlock
Text=",
" />
<TextBlock
Text="{Binding
MiddleName}"
/>
</StackPanel>
<StackPanel
Grid.Row="1"
Grid.Column="1"
Orientation="Horizontal"
VerticalAlignment="Center">
<TextBlock
Text="Grade
Point Average = " />
<TextBlock
Text="{Binding
GradePointAverage}"
/>
</StackPanel>
</Grid>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</Grid>
In this template, the height of the individual items is governed by the
explicit Height setting on the Image element. To prevent the text from moving to
the right as the photos are being loaded, an explicit Width
setting is also provided. Here's the result:
The DataTemplate Bar Chart
With a combination of a
DataTemplate and an
ItemsPanelTemplate,
you can make a ListBox
or ItemsControl
look like no other
ListBox or
ItemsControl
you've ever seen.
Let's create a new project, the GpaBarChart project
shows one approach. It has the
StudentBodyPresenter and two converters I mentioned
defined as resources:
<phone:PhoneApplicationPage.Resources>
<elpaso:StudentBodyPresenter
x:Key="studentBodyPresenter"
/>
<petzold:MultiplyConverter
x:Key="multiply"
/>
<petzold:ValueToBrushConverter
x:Key="valueToBrush"
Criterion="1"
GreaterThanBrush="{StaticResource
PhoneAccentBrush}"
EqualToBrush="{StaticResource
PhoneAccentBrush}"
LessThanBrush="Red"
/>
</phone:PhoneApplicationPage.Resources>
Most of the content area you've already seen but I also added a Border with
the name "studentDisplay" floating near the top. This Border includes a couple
TextBlock elements with their Text properties bound to the properties FullName
and GradePointAverage under the assumption that the DataContext of this Border
is an object of type Student. That's not normally the case, so the Border has
its Visibility property initialized to Collapsed:
<Grid
x:Name="ContentPanel"
Grid.Row="1"
Margin="12,0,12,0"
DataContext="{Binding
Source={StaticResource
studentBodyPresenter},
Path=StudentBody}">
<Border
x:Name="studentDisplay"
BorderBrush="{StaticResource
PhoneForegroundBrush}"
BorderThickness="{StaticResource
PhoneBorderThickness}"
HorizontalAlignment="Center"
VerticalAlignment="Top"
Margin="24"
Padding="12"
CornerRadius="24"
Visibility="Collapsed">
<StackPanel>
<TextBlock
Text="{Binding
FullName}"
HorizontalAlignment="Center"
/>
<StackPanel
Orientation="Horizontal">
<TextBlock
Text="GPA
= " />
<TextBlock
Text="{Binding
GradePointAverage}"
/>
</StackPanel>
</StackPanel>
</Border>
<ItemsControl
ItemsSource="{Binding
Students}"
VerticalAlignment="Bottom">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Rectangle
Fill="{Binding
GradePointAverage,
Converter={StaticResource
valueToBrush}}"
Height="{Binding
GradePointAverage,
Converter={StaticResource
multiply},
ConverterParameter=50}"
VerticalAlignment="Bottom"
Margin="1
0" />
</DataTemplate>
</ItemsControl.ItemTemplate>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<petzold:UniformStack
Orientation="Horizontal"
/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
</Grid>
The code-behind file fills in the missing logic. The page processes the
Touch.FrameReported event. When the element directly behind the primary touch
point is a Rectangle, the event handler obtains the DataContext of that
Rectangle. That is an object of type Student. That object is then set to the
DataContext of the Border. The TouchAction property is used to turn the
Visibility on and off:
namespace
GpaBarChart
{
public partial
class MainPage
: PhoneApplicationPage
{
public MainPage()
{
InitializeComponent();
Touch.FrameReported +=
OnTouchFrameReported;
}
void OnTouchFrameReported(object
sender, TouchFrameEventArgs args)
{
TouchPoint touchPoint =
args.GetPrimaryTouchPoint(this);
if (touchPoint !=
null && touchPoint.Action ==
TouchAction.Down)
args.SuspendMousePromotionUntilTouchUp();
if (touchPoint !=
null && touchPoint.TouchDevice.DirectlyOver
is Rectangle)
{
Rectangle rectangle = (touchPoint.TouchDevice.DirectlyOver
as Rectangle);
// This DataContext is an object
of type Student
object dataContext =
rectangle.DataContext;
studentDisplay.DataContext = dataContext;
if (touchPoint.Action ==
TouchAction.Down)
studentDisplay.Visibility =
Visibility.Visible;
else
if (touchPoint.Action ==
TouchAction.Up)
studentDisplay.Visibility =
Visibility.Collapsed;
}
}
}
}
As you run your fingers across the bars, you can see the student that each
bar represents: